package com.th.coupon.service.impl;

import com.alibaba.fastjson.JSON;
import com.th.coupon.common.constant.Constant;
import com.th.coupon.common.exception.BaseCloudException;
import com.th.coupon.common.vo.response.ExceptionResultData;
import com.th.coupon.constant.CouponStatus;
import com.th.coupon.entity.Coupon;
import com.th.coupon.service.IRedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.jws.Oneway;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 *  redis相关操作的服务实现
 * @author 涂航
 */
@Service
@Slf4j
public class RedisServiceImpl implements IRedisService {

    /**
     * redis客户端
     */
    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public RedisServiceImpl(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 根据用户id 和状态找到缓存的优惠卷列表数据
     * @param userId 用户id
     * @param status 优惠卷状态
     * @return
     */
    @Override
    public List<Coupon> getCachedCoupons(Long userId, Integer status) {
        log.info("Get Coupon From Cache : {} , {}",userId,status);

        String redisKey = this.status2RedisKey(status,userId);
        //拿到Hash的Value构成的集合
        List<String> couponStr = stringRedisTemplate.opsForHash().values(redisKey)
                .stream()
                .map(o -> Objects.toString(o,null))
                .collect(Collectors.toList());
        if(CollectionUtils.isNotEmpty(couponStr)){
            saveEmptyCouponListToCache(userId,Collections.singletonList(status));
            return Collections.emptyList();
        }
        return couponStr
                .stream()
                .map(cs -> JSON.parseObject(cs,Coupon.class))
                .collect(Collectors.toList());
    }

    /**
     * 保存一个空的列表到缓存中 防止缓冲穿透
     * @param userId 用户id
     * @param status 优惠卷状态
     */
    @Override
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {
        log.info("Save Empty List To Cache For User: {},Status: {}",userId, JSON.toJSONString(status));
        //key 是 coupon_id ,value 是序列化的 Coupon
        Map<String, String> invalidCouponMap = new HashMap<>();
        invalidCouponMap.put("-1", JSON.toJSONString(Coupon.invalidCoupon()));
        //使用SessionCallback 把数据命令放入到 redis 的pipeline 批处理
        SessionCallback<Object> objectSessionCallback = new SessionCallback<Object>(){
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                status.forEach(s->{
                    String redisKey = status2RedisKey(s, userId);
                    redisOperations.opsForHash().putAll(redisKey,invalidCouponMap);
                });
                return null;
            }
        };
        log.info("Pipeline Exe Result: {}",JSON.toJSONString(stringRedisTemplate.executePipelined(objectSessionCallback)));
    }

    /**
     * 尝试到redis中获取到优惠卷码
     * @param templateId
     * @return
     */
    @Override
    public String tryToAcquireCouponCodeFromCache(Integer templateId) {
        String redisKey = String.format("%s%s",Constant.RedisPrefix.COUPON_TEMPLATE,templateId);
        //优惠卷码不存在顺序关系 左右不存在
        String couponCode = stringRedisTemplate.opsForList().leftPop(redisKey);

        log.info("Acquire Coupon Code : {} ,{} ,{}",templateId,redisKey,couponCode);

        return couponCode;
    }

    /**
     * 保存优惠卷到服务
     * @param userId 用户id
     * @param coupons 优惠卷集合 可能是一个也可能是多个
     * @param status 优惠卷状态
     * @return
     */
    @Override
    public Integer addCouponToCache(Long userId, List<Coupon> coupons, Integer status) throws BaseCloudException{
        log.info("Add Coupon To Cache : {} ,{} ,{}",userId,JSON.toJSONString(coupons),status);

        Integer result = -1;

        //不同的优惠卷存储
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus){
            case USABLE:
                result = addCouponToCacheForUsable(userId,coupons);
                break;
            case USED:
                result = addCouponToCacheForUsed(userId,coupons);
                break;
            case EXPIRED:
                result = addCouponToCacheForExpired(userId,coupons);
                break;
        }
        return result;
    }

    /**
     * 将以使用的优惠卷加入到Cache中
     * @param userId 用户id
     * @param coupons 已使用的优惠卷实体
     * @return 加入记录数
     * @throws BaseCloudException
     */
    private Integer addCouponToCacheForUsed(Long userId,List<Coupon> coupons) throws BaseCloudException{
        //如果 status 是 USED ,代表用户操作是使用当前的优惠卷 ，影响到两个 cache
        //USABLE、USED
        log.debug("Add Coupon To Cache For USED !!!");
        //存储已使用优惠卷信息
        Map<String,String> needCacheForUsed = new HashMap<>(coupons.size());
        //可使用优惠卷缓存key
        String redisKeyForUsable = status2RedisKey(CouponStatus.USABLE.getCode(), userId);
        //已使用优惠卷缓存key
        String redisKeyForUsed = status2RedisKey(CouponStatus.USED.getCode(), userId);
        //获取当前可用的优惠卷
        List<Coupon> curUsableCoupons = getCachedCoupons(userId, CouponStatus.USABLE.getCode());

        //当前可用的优惠卷个数至少大于1
        assert curUsableCoupons.size() > coupons.size();

        //将准备使用的优惠卷转存到Map中
        coupons.forEach(c -> needCacheForUsed.put(c.getId().toString(),JSON.toJSONString(c)));

        //效验当前优惠卷的参数是否与Cached中的匹配
        //缓存中可使用的优惠卷id
        List<Integer> curUsableIds = curUsableCoupons
                .stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());
        //准备使用的优惠卷id
        List<Integer> paramIds = coupons
                .stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());
        //判断当前的需要使用的优惠卷是否存在于可用优惠卷缓存中
        if(!CollectionUtils.isSubCollection(paramIds,curUsableIds)){
            log.error("CurCoupons Is Not Equals ToCache: {},{},{}",userId,JSON.toJSONString(curUsableIds),JSON.toJSONString(paramIds));
            throw new BaseCloudException(ExceptionResultData.CURCOUPONS_IS_NOT_EQUAL_CACHE);
        }
        //需要向可用优惠卷缓存中情空的优惠卷id
        List<String> needCleanKey = paramIds.stream()
                .map(i -> i.toString()).collect(Collectors.toList());

        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public  Object execute(RedisOperations redisOperations) throws DataAccessException {
                //1.添加已使用优惠卷缓存
                redisOperations.opsForHash().putAll(redisKeyForUsed,needCacheForUsed);
                //2.清理可用的优惠缓存
                redisOperations.opsForHash().delete(redisKeyForUsable,needCleanKey.toArray());
                //3.重置过期时间
                redisOperations.expire(redisKeyForUsable,getRandomExpirationTime(1,2),TimeUnit.SECONDS);
                redisOperations.expire(redisKeyForUsed,getRandomExpirationTime(1,2),TimeUnit.SECONDS);
                return null;
            }
        };

        log.info("Pipeline Exe Result : {}",JSON.toJSONString(stringRedisTemplate.executePipelined(sessionCallback)));
        return coupons.size();

    }
    /**
     * 新增加优惠卷到redis中 刚刚新增，没有使用的优惠卷
     * @param userId 用户id
     * @param coupons 新增的优惠卷实体
     * @return 加入记录数
     */
    private Integer addCouponToCacheForUsable(Long userId,List<Coupon> coupons) {
        //如果 status 是 USABLE ,代表是新增加的优惠卷
        //只会影响一个 Cache： USER_COUPON_USABLE
        log.debug("Add Coupon To Cache For Usable.");

        Map<String,String> needCacheObject = new HashMap<>();

        coupons.forEach(c ->{
            needCacheObject.put(c.getId().toString(), JSON.toJSONString(c));
        });
        String redisKey = status2RedisKey(CouponStatus.USABLE.getCode(),userId);

        stringRedisTemplate.opsForHash().putAll(redisKey,needCacheObject);
        log.info("Add {} Coupons To Cache : {} , {}",needCacheObject.size(),userId,redisKey);
        stringRedisTemplate.expire(redisKey,getRandomExpirationTime(1,2), TimeUnit.SECONDS);
        return needCacheObject.size();
    }

    /**
     * 将过期优惠卷加入到redis
     * @param userId 用户id
     * @param coupons 过期的优惠实体
     * @return
     * @throws BaseCloudException
     */
    private Integer addCouponToCacheForExpired(Long userId,List<Coupon> coupons) throws BaseCloudException{
        //Status 是 EXPIRED 代表是已有优惠卷鼓起了 ，影响到两个 cache
        //USABLE ,EXPIRED

        log.debug("Add Coupon To Cache For Expired !!!");

        //存储已过期优惠卷信息
        Map<String,String> needCacheForUsed = new HashMap<>(coupons.size());
        //可使用优惠卷缓存key
        String redisKeyForUsable = status2RedisKey(CouponStatus.USABLE.getCode(), userId);
        //已过期优惠卷缓存key
        String redisKeyForExpired = status2RedisKey(CouponStatus.EXPIRED.getCode(), userId);
        //获取当前可用的优惠卷
        List<Coupon> curUsableCoupons = getCachedCoupons(userId, CouponStatus.USABLE.getCode());
        //获取当前以过期的优惠卷
        List<Coupon> curExpiredCoupons = getCachedCoupons(userId, CouponStatus.EXPIRED.getCode());
        //当前可用的优惠卷个数是大于1的
        assert curUsableCoupons.size() > coupons.size();

        //将过期优惠卷转存到Map中
        coupons.forEach(c -> needCacheForUsed.put(c.getId().toString(),JSON.toJSONString(c)));

        //效验当前优惠卷的参数是否与Cached中的匹配
        //缓存中可使用的优惠卷id
        List<Integer> curUsableIds = curUsableCoupons
                .stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());
        //过期的优惠卷id
        List<Integer> paramIds = coupons
                .stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());
        //判断当前的需要使用的优惠卷是否存在于可用优惠卷缓存中
        if(!CollectionUtils.isSubCollection(paramIds,curUsableIds)){
            log.error("CurCoupons Is Not Equals ToCache: {},{},{}",userId,JSON.toJSONString(curUsableIds),JSON.toJSONString(paramIds));
            throw new BaseCloudException(ExceptionResultData.CURCOUPONS_IS_NOT_EQUAL_CACHE);
        }
        //需要向可用的优惠卷缓存中清空的优惠卷id
        List<String> needCleanKey = paramIds.stream()
                .map(i -> i.toString()).collect(Collectors.toList());

        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public  Object execute(RedisOperations redisOperations) throws DataAccessException {
                //1.添加过期优惠卷缓存
                redisOperations.opsForHash().putAll(redisKeyForExpired,needCacheForUsed);
                //2.清理可用的优惠缓存
                redisOperations.opsForHash().delete(redisKeyForUsable,needCleanKey.toArray());
                //3.重置过期时间
                redisOperations.expire(redisKeyForUsable,getRandomExpirationTime(1,2),TimeUnit.SECONDS);
                redisOperations.expire(redisKeyForExpired,getRandomExpirationTime(1,2),TimeUnit.SECONDS);
                return null;
            }
        };

        log.info("Pipeline Exe Result : {}",JSON.toJSONString(stringRedisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }

    /**
     * 获取随机的过期时间 解决缓存雪崩问题 : key在同一时间失效
     * @param min 最小的小时数
     * @param max 最大的小时数
     * @return 返回 [min,max] 之间的随机秒数
     */
    private Long getRandomExpirationTime(Integer min,Integer max){

        return RandomUtils.nextLong(min*60*60,max*60*60);
    }

    /**
     * 根据 status获取到对应的redis key
     * @param status
     * @param userId
     * @return
     */
    private String status2RedisKey(Integer status,Long userId){
        String redisKey = null;

        CouponStatus of = CouponStatus.of(status);
        switch (of){
            case USABLE:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USABLE,userId);
                break;
            case USED:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USED,userId);
                break;
            case EXPIRED:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_EXPIRED,userId);
                break;
        }

        return redisKey;
    }
}
